/**
 *    Copyright 2009-2019 the original author or authors.
 *
 *    Licensed under the Apache License, Version 2.0 (the "License");
 *    you may not use this file except in compliance with the License.
 *    You may obtain a copy of the License at
 *
 *       http://www.apache.org/licenses/LICENSE-2.0
 *
 *    Unless required by applicable law or agreed to in writing, software
 *    distributed under the License is distributed on an "AS IS" BASIS,
 *    WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 *    See the License for the specific language governing permissions and
 *    limitations under the License.
 */
package org.apache.ibatis.builder.xml;

import org.apache.ibatis.builder.BaseBuilder;
import org.apache.ibatis.builder.MapperBuilderAssistant;
import org.apache.ibatis.executor.keygen.Jdbc3KeyGenerator;
import org.apache.ibatis.executor.keygen.KeyGenerator;
import org.apache.ibatis.executor.keygen.NoKeyGenerator;
import org.apache.ibatis.executor.keygen.SelectKeyGenerator;
import org.apache.ibatis.mapping.*;
import org.apache.ibatis.parsing.XNode;
import org.apache.ibatis.scripting.LanguageDriver;
import org.apache.ibatis.session.Configuration;

import java.util.List;
import java.util.Locale;

/**
 * Mybatis声明语句解析器
 * @author Clinton Begin
 */
public class XMLStatementBuilder extends BaseBuilder {
    /**
     * Mapper解析助手
     */
    private final MapperBuilderAssistant builderAssistant;
    /**
     * Statement节点
     */
    private final XNode context;
    /**
     * 必须匹配的数据库类型唯一标志
     */
    private final String requiredDatabaseId;

    public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context) {
        this(configuration, builderAssistant, context, null);
    }

    public XMLStatementBuilder(Configuration configuration, MapperBuilderAssistant builderAssistant, XNode context, String databaseId) {
        super(configuration);
        this.builderAssistant = builderAssistant;
        this.context = context;
        this.requiredDatabaseId = databaseId;
    }

    /**
     * 完成指定Statement的解析操作
     */
    public void parseStatementNode() {
        // step1: 基础属性的取值操作

        // 获取声明语句的唯一标志
        String id = context.getStringAttribute("id");

        // 获取对应的数据库唯一标志
        String databaseId = context.getStringAttribute("databaseId");
        // 校验数据库类型是否匹配
        if (!databaseIdMatchesCurrent(id, databaseId, this.requiredDatabaseId)) {
            // 如果数据库类型不一致，跳过处理
            return;
        }

        // 获取内容的大小限制
        Integer fetchSize = context.getIntAttribute("fetchSize");
        // 超时时间
        Integer timeout = context.getIntAttribute("timeout");
        // 参数映射
        String parameterMap = context.getStringAttribute("parameterMap");
        // 参数类型
        String parameterType = context.getStringAttribute("parameterType");
        // 解析参数类
        Class<?> parameterTypeClass = resolveClass(parameterType);
        // 响应映射
        String resultMap = context.getStringAttribute("resultMap");
        // 响应类型
        String resultType = context.getStringAttribute("resultType");
        // 语言类型
        String lang = context.getStringAttribute("lang");
        // 获取语言驱动
        LanguageDriver langDriver = getLanguageDriver(lang);

        // 解析响应类类型
        Class<?> resultTypeClass = resolveClass(resultType);

        // 解析不可重复响应集合
        String resultSetType = context.getStringAttribute("resultSetType");
        // 解析语句的声明类型，mybatis目前支持三种,prepare、硬编码、以及存储过程调用
        StatementType statementType = StatementType.valueOf(context.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        // 解析响应集合类型
        ResultSetType resultSetTypeEnum = resolveResultSetType(resultSetType);

        String nodeName = context.getNode().getNodeName();
        // 根据节点的名称获取SQL语句类型
        SqlCommandType sqlCommandType = SqlCommandType.valueOf(nodeName.toUpperCase(Locale.ENGLISH));
        // 判断是否为查询语句
        boolean isSelect = sqlCommandType == SqlCommandType.SELECT;
        // 是否刷新缓存，非查询语句(insert|update|delete)才会刷新缓存
        boolean flushCache = context.getBooleanAttribute("flushCache", !isSelect);
        // 是否应用缓存，查询语句(select)才会应用缓存
        boolean useCache = context.getBooleanAttribute("useCache", isSelect);

        // 查询结果是否有序且成组
        boolean resultOrdered = context.getBooleanAttribute("resultOrdered", false);


        // step2: 特殊元素的解析操作

        // 解析内部的Include标签
        // Include Fragments before parsing
        XMLIncludeTransformer includeParser = new XMLIncludeTransformer(configuration, builderAssistant);
        // 解析并处理include标签
        includeParser.applyIncludes(context.getNode());

        // 解析select声明语句的selectKey内容
        // Parse selectKey after includes and remove them.
        processSelectKeyNodes(id, parameterTypeClass, langDriver);

        // 解析SQL内容
        // Parse the SQL (pre: <selectKey> and <include> were parsed and removed)
        SqlSource sqlSource = langDriver.createSqlSource(configuration, context, parameterTypeClass);

        // 获取select声明语句的resultSets属性声明
        String resultSets = context.getStringAttribute("resultSets");
        // 获取update声明语句的resultSets属性声明
        String keyProperty = context.getStringAttribute("keyProperty");
        // 获取insert声明语句的keyColumn属性声明
        String keyColumn = context.getStringAttribute("keyColumn");

        // 配置主键生成器
        KeyGenerator keyGenerator;
        String keyStatementId = id + SelectKeyGenerator.SELECT_KEY_SUFFIX;
        // 合并命名空间
        keyStatementId = builderAssistant.applyCurrentNamespace(keyStatementId, true);
        // 获取主键生成器
        if (configuration.hasKeyGenerator(keyStatementId)) {
            // 已有直接获取
            keyGenerator = configuration.getKeyGenerator(keyStatementId);
        } else {
            // 没有则生成
            keyGenerator = context.getBooleanAttribute("useGeneratedKeys",
                    configuration.isUseGeneratedKeys() && SqlCommandType.INSERT.equals(sqlCommandType))
                    ? Jdbc3KeyGenerator.INSTANCE : NoKeyGenerator.INSTANCE;
        }
        // step3: MappedStatement对象的构建工作

        // 构建整体配置
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, resultSets);
    }

    private void processSelectKeyNodes(String id, Class<?> parameterTypeClass, LanguageDriver langDriver) {
        // 获取selectKey节点
        List<XNode> selectKeyNodes = context.evalNodes("selectKey");
        if (configuration.getDatabaseId() != null) {
            // 解析指定为当前数据库类型的SelectKey
            parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, configuration.getDatabaseId());
        }
        // 解析所有未指定数据库类型的SelectKey
        parseSelectKeyNodes(id, selectKeyNodes, parameterTypeClass, langDriver, null);

        // 移除所有selectKey节点
        removeSelectKeyNodes(selectKeyNodes);
    }

    /**
     * 解析selectKey节点
     *
     * @param parentId             父节点
     * @param list                 所有的selectKey
     * @param parameterTypeClass   参数类型
     * @param langDriver           语言驱动
     * @param skRequiredDatabaseId 必须的数据库类型唯一标志
     */
    private void parseSelectKeyNodes(String parentId, List<XNode> list, Class<?> parameterTypeClass, LanguageDriver langDriver, String skRequiredDatabaseId) {
        for (XNode nodeToHandle : list) {
            // 生成selectKey唯一标志
            String id = parentId + SelectKeyGenerator.SELECT_KEY_SUFFIX;
            // 获取数据库标志
            String databaseId = nodeToHandle.getStringAttribute("databaseId");

            if (databaseIdMatchesCurrent(id, databaseId, skRequiredDatabaseId)) {
                // 解析SelectKey节点
                parseSelectKeyNode(id, nodeToHandle, parameterTypeClass, langDriver, databaseId);
            }
        }
    }

    /**
     * 解析SelectKey节点
     *
     * @param id                 selectKey唯一标志
     * @param nodeToHandle       需要处理的selectKey节点
     * @param parameterTypeClass 参数类型
     * @param langDriver         语言驱动
     * @param databaseId         数据库类型唯一标志
     */
    private void parseSelectKeyNode(String id, XNode nodeToHandle, Class<?> parameterTypeClass, LanguageDriver langDriver, String databaseId) {
        // 获取返回类型
        String resultType = nodeToHandle.getStringAttribute("resultType");
        // 解析出返回类型的实际类型
        Class<?> resultTypeClass = resolveClass(resultType);
        // 解析声明语句类型
        StatementType statementType = StatementType.valueOf(nodeToHandle.getStringAttribute("statementType", StatementType.PREPARED.toString()));
        // 解析key值
        String keyProperty = nodeToHandle.getStringAttribute("keyProperty");
        // 解析key指端
        String keyColumn = nodeToHandle.getStringAttribute("keyColumn");
        // 解析在语句之前还是之后执行
        boolean executeBefore = "BEFORE".equals(nodeToHandle.getStringAttribute("order", "AFTER"));

        //defaults
        // 不使用缓存
        boolean useCache = false;
        boolean resultOrdered = false;
        // 无主键生成器
        KeyGenerator keyGenerator = NoKeyGenerator.INSTANCE;
        Integer fetchSize = null;
        Integer timeout = null;
        boolean flushCache = false;
        String parameterMap = null;
        String resultMap = null;
        // 定义查询结果ResultSet的类别
        ResultSetType resultSetTypeEnum = null;

        /*
                ===============================================================================================
                创建SqlSource
                ===============================================================================================
         */
        // 解析SQL语句
        SqlSource sqlSource = langDriver.createSqlSource(configuration, nodeToHandle, parameterTypeClass);

        // 解析SQL命令类型，SelectKey必然为查询(SELECT)
        SqlCommandType sqlCommandType = SqlCommandType.SELECT;

        // 添加声明语句的映射关系
        builderAssistant.addMappedStatement(id, sqlSource, statementType, sqlCommandType,
                fetchSize, timeout, parameterMap, parameterTypeClass, resultMap, resultTypeClass,
                resultSetTypeEnum, flushCache, useCache, resultOrdered,
                keyGenerator, keyProperty, keyColumn, databaseId, langDriver, null);

        // 合并命名空间
        id = builderAssistant.applyCurrentNamespace(id, false);

        // 获取声明语句,这个位置比较有意思，builderAssistant.addMappedStatement方法可以直接返回生成的MappedStatement对象的。
        // TODO  暂存疑惑不知为何不直接使用返回值而是重新获取，难道是为了刷新未完成解析的声明语句？？？
        MappedStatement keyStatement = configuration.getMappedStatement(id, false);

        // 配置主键生成器
        configuration.addKeyGenerator(id, new SelectKeyGenerator(keyStatement, executeBefore));
    }

    private void removeSelectKeyNodes(List<XNode> selectKeyNodes) {
        for (XNode nodeToHandle : selectKeyNodes) {
            nodeToHandle.getParent().getNode().removeChild(nodeToHandle.getNode());
        }
    }

    private boolean databaseIdMatchesCurrent(String id, String databaseId, String requiredDatabaseId) {
        if (requiredDatabaseId != null) {
            if (!requiredDatabaseId.equals(databaseId)) {
                return false;
            }
        } else {
            if (databaseId != null) {
                return false;
            }
            // skip this statement if there is a previous one with a not null databaseId
            id = builderAssistant.applyCurrentNamespace(id, false);
            if (this.configuration.hasStatement(id, false)) {

                MappedStatement previous = this.configuration.getMappedStatement(id, false); // issue #2
                if (previous.getDatabaseId() != null) {
                    return false;
                }
            }
        }
        return true;
    }

    private LanguageDriver getLanguageDriver(String lang) {
        Class<? extends LanguageDriver> langClass = null;
        if (lang != null) {
            // 利用别名注册机制获取语言解析器实例类型
            langClass = resolveClass(lang);
        }
        return builderAssistant.getLanguageDriver(langClass);
    }

}
